2 位追蹤者

模型

模型是 MVC 架構的一部分。它們是代表業務資料、規則和邏輯的物件。

您可以透過擴充 yii\base\Model 或其子類別來建立模型類別。基礎類別 yii\base\Model 支援許多有用的功能

  • 屬性:代表業務資料,可以像一般的物件屬性或陣列元素一樣存取;
  • 屬性標籤:指定屬性的顯示標籤;
  • 大量賦值:支援在單一步驟中填入多個屬性;
  • 驗證規則:根據宣告的驗證規則確保輸入資料;
  • 資料匯出:允許以可自訂格式的陣列形式匯出模型資料。

Model 類別也是更進階模型的基礎類別,例如 Active Record。請參閱相關文件,以取得關於這些進階模型的更多詳細資訊。

資訊:您不需要將模型類別基於 yii\base\Model。然而,由於有許多 Yii 組件是為了支援 yii\base\Model 而建立的,因此它通常是模型的較佳基礎類別。

屬性

模型以屬性的形式代表業務資料。每個屬性都像是模型的一個公開存取的屬性。yii\base\Model::attributes() 方法指定了模型類別擁有哪些屬性。

您可以像存取一般物件屬性一樣存取屬性

$model = new \app\models\ContactForm;

// "name" is an attribute of ContactForm
$model->name = 'example';
echo $model->name;

您也可以像存取陣列元素一樣存取屬性,這要歸功於 yii\base\ModelArrayAccessTraversable 的支援

$model = new \app\models\ContactForm;

// accessing attributes like array elements
$model['name'] = 'example';
echo $model['name'];

// Model is traversable using foreach.
foreach ($model as $name => $value) {
    echo "$name: $value\n";
}

定義屬性

預設情況下,如果您的模型類別直接從 yii\base\Model 擴充,則其所有非靜態公開成員變數都是屬性。例如,下面的 ContactForm 模型類別有四個屬性:nameemailsubjectbodyContactForm 模型用於表示從 HTML 表單接收的輸入資料。

namespace app\models;

use yii\base\Model;

class ContactForm extends Model
{
    public $name;
    public $email;
    public $subject;
    public $body;
}

您可以覆寫 yii\base\Model::attributes() 以不同的方式定義屬性。該方法應傳回模型中屬性的名稱。例如,yii\db\ActiveRecord 透過傳回相關資料庫表格的欄位名稱作為其屬性名稱來做到這一點。請注意,您可能還需要覆寫 magic methods,例如 __get()__set(),以便可以像一般物件屬性一樣存取屬性。

屬性標籤

在顯示值或取得屬性的輸入時,您經常需要顯示與屬性相關聯的一些標籤。例如,給定一個名為 firstName 的屬性,您可能想要顯示一個標籤 First Name,當在表單輸入和錯誤訊息等位置向終端使用者顯示時,它會更友善。

您可以透過呼叫 yii\base\Model::getAttributeLabel() 來取得屬性的標籤。例如,

$model = new \app\models\ContactForm;

// displays "Name"
echo $model->getAttributeLabel('name');

預設情況下,屬性標籤會從屬性名稱自動產生。產生是由方法 yii\base\Model::generateAttributeLabel() 完成的。它會將駝峰式變數名稱轉換為多個單字,每個單字的首字母大寫。例如,username 變成 Username,而 firstName 變成 First Name

如果您不想使用自動產生的標籤,您可以覆寫 yii\base\Model::attributeLabels() 來明確宣告屬性標籤。例如,

namespace app\models;

use yii\base\Model;

class ContactForm extends Model
{
    public $name;
    public $email;
    public $subject;
    public $body;

    public function attributeLabels()
    {
        return [
            'name' => 'Your name',
            'email' => 'Your email address',
            'subject' => 'Subject',
            'body' => 'Content',
        ];
    }
}

對於支援多種語言的應用程式,您可能想要翻譯屬性標籤。這也可以在 attributeLabels() 方法中完成,如下所示

public function attributeLabels()
{
    return [
        'name' => \Yii::t('app', 'Your name'),
        'email' => \Yii::t('app', 'Your email address'),
        'subject' => \Yii::t('app', 'Subject'),
        'body' => \Yii::t('app', 'Content'),
    ];
}

您甚至可以有條件地定義屬性標籤。例如,根據模型正在使用的 情境,您可以為同一個屬性傳回不同的標籤。

資訊:嚴格來說,屬性標籤是 視圖 的一部分。但是在模型中宣告標籤通常非常方便,並且可以產生非常乾淨且可重複使用的程式碼。

情境

模型可能會在不同的情境中使用。例如,User 模型可用於收集使用者登入輸入,但也可用於使用者註冊目的。在不同的情境中,模型可能會使用不同的業務規則和邏輯。例如,email 屬性在使用者註冊期間可能是必需的,但在使用者登入期間則不是。

模型使用 yii\base\Model::$scenario 屬性來追蹤它正在使用的情境。預設情況下,模型僅支援名為 default 的單一情境。以下程式碼顯示了設定模型情境的兩種方式

// scenario is set as a property
$model = new User;
$model->scenario = User::SCENARIO_LOGIN;

// scenario is set through configuration
$model = new User(['scenario' => User::SCENARIO_LOGIN]);

預設情況下,模型支援的情境由模型中宣告的 驗證規則 決定。但是,您可以透過覆寫 yii\base\Model::scenarios() 方法來自訂此行為,如下所示

namespace app\models;

use yii\db\ActiveRecord;

class User extends ActiveRecord
{
    const SCENARIO_LOGIN = 'login';
    const SCENARIO_REGISTER = 'register';

    public function scenarios()
    {
        return [
            self::SCENARIO_LOGIN => ['username', 'password'],
            self::SCENARIO_REGISTER => ['username', 'email', 'password'],
        ];
    }
}

資訊:在以上和以下範例中,模型類別是從 yii\db\ActiveRecord 擴充而來的,因為多個情境的使用通常發生在 Active Record 類別中。

scenarios() 方法傳回一個陣列,其鍵是情境名稱,值是相應的活動屬性。活動屬性可以被大量賦值,並且受 驗證 的約束。在上面的範例中,usernamepassword 屬性在 login 情境中是活動的;而在 register 情境中,除了 usernamepassword 之外,email 也是活動的。

scenarios() 的預設實作將傳回在驗證規則宣告方法 yii\base\Model::rules() 中找到的所有情境。當覆寫 scenarios() 時,如果您想要在預設情境之外引入新的情境,您可以編寫如下程式碼

namespace app\models;

use yii\db\ActiveRecord;

class User extends ActiveRecord
{
    const SCENARIO_LOGIN = 'login';
    const SCENARIO_REGISTER = 'register';

    public function scenarios()
    {
        $scenarios = parent::scenarios();
        $scenarios[self::SCENARIO_LOGIN] = ['username', 'password'];
        $scenarios[self::SCENARIO_REGISTER] = ['username', 'email', 'password'];
        return $scenarios;
    }
}

情境功能主要用於 驗證大量屬性賦值。但是,您可以將其用於其他目的。例如,您可以根據目前的情境以不同的方式宣告 屬性標籤

驗證規則

當從終端使用者接收模型的資料時,應對其進行驗證,以確保其滿足某些規則(稱為驗證規則,也稱為業務規則)。例如,給定一個 ContactForm 模型,您可能想要確保所有屬性都不是空的,並且 email 屬性包含有效的電子郵件地址。如果某些屬性的值不滿足相應的業務規則,則應顯示適當的錯誤訊息,以幫助使用者修正錯誤。

您可以呼叫 yii\base\Model::validate() 來驗證接收到的資料。該方法將使用在 yii\base\Model::rules() 中宣告的驗證規則來驗證每個相關屬性。如果未發現錯誤,它將傳回 true。否則,它會將錯誤保存在 yii\base\Model::$errors 屬性中並傳回 false。例如,

$model = new \app\models\ContactForm;

// populate model attributes with user inputs
$model->attributes = \Yii::$app->request->post('ContactForm');

if ($model->validate()) {
    // all inputs are valid
} else {
    // validation failed: $errors is an array containing error messages
    $errors = $model->errors;
}

若要宣告與模型相關聯的驗證規則,請覆寫 yii\base\Model::rules() 方法,方法是傳回模型屬性應滿足的規則。以下範例顯示了為 ContactForm 模型宣告的驗證規則

public function rules()
{
    return [
        // the name, email, subject and body attributes are required
        [['name', 'email', 'subject', 'body'], 'required'],

        // the email attribute should be a valid email address
        ['email', 'email'],
    ];
}

一個規則可以用於驗證一個或多個屬性,並且一個屬性可以由一個或多個規則驗證。請參閱 驗證輸入 章節,以取得關於如何宣告驗證規則的更多詳細資訊。

有時,您可能希望規則僅在某些 情境 中應用。若要做到這一點,您可以指定規則的 on 屬性,如下所示

public function rules()
{
    return [
        // username, email and password are all required in "register" scenario
        [['username', 'email', 'password'], 'required', 'on' => self::SCENARIO_REGISTER],

        // username and password are required in "login" scenario
        [['username', 'password'], 'required', 'on' => self::SCENARIO_LOGIN],
        
        [['username'], 'string'], // username must always be a string, this rule applies to all scenarios
    ];
}

如果您不指定 on 屬性,則該規則將在所有情境中應用。如果規則可以在目前 情境 中應用,則該規則稱為活動規則

屬性將僅在以下情況下進行驗證:它是 scenarios() 中宣告的活動屬性,並且與 rules() 中宣告的一個或多個活動規則相關聯。

大量賦值

大量賦值是一種使用單行程式碼填入模型與使用者輸入的便捷方式。它透過將輸入資料直接賦值給 yii\base\Model::$attributes 屬性來填入模型的屬性。以下兩段程式碼是等效的,都試圖將終端使用者提交的表單資料賦值給 ContactForm 模型的屬性。顯然,前者(使用大量賦值)比後者更乾淨且不易出錯

$model = new \app\models\ContactForm;
$model->attributes = \Yii::$app->request->post('ContactForm');
$model = new \app\models\ContactForm;
$data = \Yii::$app->request->post('ContactForm', []);
$model->name = isset($data['name']) ? $data['name'] : null;
$model->email = isset($data['email']) ? $data['email'] : null;
$model->subject = isset($data['subject']) ? $data['subject'] : null;
$model->body = isset($data['body']) ? $data['body'] : null;

安全屬性

大量賦值僅適用於所謂的安全屬性,這些屬性是在模型目前 情境yii\base\Model::scenarios() 中列出的屬性。例如,如果 User 模型具有以下情境宣告,則當目前情境為 login 時,只能大量賦值 usernamepassword。任何其他屬性都將保持不變。

public function scenarios()
{
    return [
        self::SCENARIO_LOGIN => ['username', 'password'],
        self::SCENARIO_REGISTER => ['username', 'email', 'password'],
    ];
}

資訊:大量賦值僅適用於安全屬性的原因是您想要控制哪些屬性可以由終端使用者資料修改。例如,如果 User 模型具有決定指派給使用者的權限的 permission 屬性,您會希望此屬性只能由管理員透過後端介面修改。

由於 yii\base\Model::scenarios() 的預設實作將傳回在 yii\base\Model::rules() 中找到的所有情境和屬性,因此如果您不覆寫此方法,則表示只要屬性出現在其中一個活動驗證規則中,它就是安全的。

因此,提供了一個別名為 safe 的特殊驗證器,以便您可以宣告屬性為安全,而無需實際驗證它。例如,以下規則宣告 titledescription 都是安全屬性。

public function rules()
{
    return [
        [['title', 'description'], 'safe'],
    ];
}

不安全屬性

如上所述,yii\base\Model::scenarios() 方法有兩個用途:決定應驗證哪些屬性,以及決定哪些屬性是安全的。在某些罕見的情況下,您可能想要驗證屬性,但不想要將其標記為安全。您可以透過在 scenarios() 中宣告屬性時,在屬性名稱前加上驚嘆號 ! 來做到這一點,例如以下程式碼中的 secret 屬性

public function scenarios()
{
    return [
        self::SCENARIO_LOGIN => ['username', 'password', '!secret'],
    ];
}

當模型處於 login 情境時,所有三個屬性都將被驗證。但是,只有 usernamepassword 屬性可以大量賦值。若要將輸入值賦值給 secret 屬性,您必須明確地執行,如下所示,

$model->secret = $secret;

同樣可以在 rules() 方法中完成

public function rules()
{
    return [
        [['username', 'password', '!secret'], 'required', 'on' => 'login']
    ];
}

在這種情況下,屬性 usernamepasswordsecret 是必需的,但 secret 必須明確賦值。

資料匯出

模型通常需要以不同的格式匯出。例如,您可能想要將模型集合轉換為 JSON 或 Excel 格式。匯出過程可以分解為兩個獨立的步驟

  • 模型轉換為陣列;
  • 陣列轉換為目標格式。

您可以只專注於第一步,因為第二步可以透過通用資料格式化器來實現,例如 yii\web\JsonResponseFormatter

將模型轉換為陣列的最簡單方法是使用 yii\base\Model::$attributes 屬性。例如,

$post = \app\models\Post::findOne(100);
$array = $post->attributes;

預設情況下,yii\base\Model::$attributes 屬性將傳回在 yii\base\Model::attributes() 中宣告的所有屬性的值。

將模型轉換為陣列的更靈活且更強大的方法是使用 yii\base\Model::toArray() 方法。其預設行為與 yii\base\Model::$attributes 相同。但是,它允許您選擇要將哪些資料項目(稱為欄位)放入結果陣列中,以及應如何格式化它們。實際上,它是 RESTful Web 服務開發中匯出模型的預設方式,如 回應格式化 中所述。

欄位

欄位簡而言之就是透過呼叫模型的 yii\base\Model::toArray() 方法獲得的陣列中的具名元素。

預設情況下,欄位名稱等同於屬性名稱。但是,您可以透過覆寫 fields() 和/或 extraFields() 方法來變更此行為。這兩種方法都應傳回欄位定義的清單。由 fields() 定義的欄位是預設欄位,這表示 toArray() 將預設傳回這些欄位。extraFields() 方法定義了額外可用的欄位,只要您透過 $expand 參數指定它們,也可以由 toArray() 傳回。例如,以下程式碼將傳回 fields() 中定義的所有欄位,以及 prettyNamefullAddress 欄位(如果它們在 extraFields() 中定義)。

$array = $model->toArray([], ['prettyName', 'fullAddress']);

您可以覆寫 fields() 以新增、移除、重新命名或重新定義欄位。fields() 的傳回值應為陣列。陣列鍵是欄位名稱,陣列值是對應的欄位定義,它可以是屬性/屬性名稱或傳回對應欄位值的匿名函式。在欄位名稱與其定義屬性名稱相同的情況下,您可以省略陣列鍵。例如,

// explicitly list every field, best used when you want to make sure the changes
// in your DB table or model attributes do not cause your field changes (to keep API backward compatibility).
public function fields()
{
    return [
        // field name is the same as the attribute name
        'id',

        // field name is "email", the corresponding attribute name is "email_address"
        'email' => 'email_address',

        // field name is "name", its value is defined by a PHP callback
        'name' => function () {
            return $this->first_name . ' ' . $this->last_name;
        },
    ];
}

// filter out some fields, best used when you want to inherit the parent implementation
// and exclude some sensitive fields.
public function fields()
{
    $fields = parent::fields();

    // remove fields that contain sensitive information
    unset($fields['auth_key'], $fields['password_hash'], $fields['password_reset_token']);

    return $fields;
}

警告:由於預設情況下,模型的所有屬性都將包含在匯出的陣列中,因此您應檢查您的資料,以確保它們不包含敏感資訊。如果存在此類資訊,您應覆寫 fields() 以將其篩選掉。在上面的範例中,我們選擇篩選掉 auth_keypassword_hashpassword_reset_token

最佳實務

模型是代表業務資料、規則和邏輯的核心位置。它們經常需要在不同的地方重複使用。在設計良好的應用程式中,模型通常比 控制器 更龐大。

總之,模型

  • 可能包含代表業務資料的屬性;
  • 可能包含驗證規則,以確保資料的有效性和完整性;
  • 可能包含實作業務邏輯的方法;
  • 不應直接存取請求、工作階段或任何其他環境資料。這些資料應由 控制器 注入到模型中;
  • 應避免嵌入 HTML 或其他表示程式碼 - 這最好在 視圖 中完成;
  • 避免在單一模型中擁有太多 情境

當您開發大型複雜系統時,通常可以考慮上述最後一個建議。在這些系統中,模型可能非常龐大,因為它們在許多地方使用,因此可能包含許多規則和業務邏輯集。這通常最終會導致維護模型程式碼的夢魘,因為對程式碼的單一觸摸可能會影響幾個不同的地方。為了使模型程式碼更易於維護,您可以採取以下策略

  • 定義一組由不同 應用程式模組 共用的基礎模型類別。這些模型類別應包含所有用法通用的最小規則和邏輯集。
  • 在使用模型的每個 應用程式模組 中,透過從相應的基礎模型類別擴充來定義具體的模型類別。具體的模型類別應包含特定於該應用程式或模組的規則和邏輯。

例如,在 進階專案範本 中,您可以定義基礎模型類別 common\models\Post。然後對於前端應用程式,您定義並使用從 common\models\Post 擴充的具體模型類別 frontend\models\Post。對於後端應用程式也是如此,您定義 backend\models\Post。透過此策略,您將確保 frontend\models\Post 中的程式碼僅特定於前端應用程式,並且如果您對其進行任何變更,則無需擔心變更是否會破壞後端應用程式。

發現錯字或您認為此頁面需要改進?
在 github 上編輯它 !